分享到

簡介

日期和時間資料通常由資料庫系統管理,而且非常重要,但正確處理起來往往比最初看起來更棘手。資料庫必須能夠以清晰、明確的格式儲存日期和時間資料,將這些資料轉換為使用者友善的格式以與用戶端應用程式互動,並在考量到不同時區和日光節約時間變化等複雜性的情況下執行基於時間的操作。

在本指南中,我們將討論 MongoDB 提供的一些工具,以便有效地處理日期和時間資料。我們將探索相關的資料類型,查看運算子和方法,並說明如何最佳地使用這些工具來妥善管理您的日期和時間資料。

MongoDB 的 DateTimestamp 類型

DATE 類型在 MongoDB 中可以將日期和時間值儲存為一個組合單位。

在這裡,左欄代表資料類型的 BSON (二進制 JSON) 名稱,第二欄代表與該類型關聯的 ID 號碼。最後的「別名」欄代表 MongoDB 用於表示類型的字串。

Type | Number | Alias |
------------------ | ------ | ------------ |
Date | 9 | "date" |

BSON Date 類型是一個帶符號的 64 位元整數,表示自 Unix 紀元 (1970 年 1 月 1 日) 以來的毫秒數。正數表示自紀元以來經過的時間,而負數表示從紀元開始倒退的時間。

將日期和時間資料儲存為大型整數是有益的,因為它

  • 允許 MongoDB 以毫秒精度儲存日期
  • 提供了日期和時間顯示方式的彈性

由於日期類型不儲存時區等額外資訊,因此如果時區相關,則必須單獨儲存該上下文。MongoDB 將在內部使用 UTC 儲存日期和時間資訊,但可以根據需要輕鬆地在檢索時轉換為其他時區。

MongoDB 還提供主要在內部使用的 Timestamp 類型

Type | Number | Alias |
------------------ | ------ | ------------ |
Timestamp | 17 | "timestamp" |

由於這主要是為了協助協調內部流程 (如複製和分片) 而實作的,因此您可能不應在自己的應用程式邏輯中使用它。日期類型通常可以滿足您可能擁有的任何時間要求。

如何建立新日期

您可以使用兩種不同的方式建立新的 Date 物件

  • new Date():以 Date 物件形式傳回日期和時間。
  • ISODate():以 Date 物件形式傳回日期和時間。

new Date()ISODate() 方法都會產生一個 Date 物件,該物件會包裝在 ISODate() 輔助函數中。

此外,在沒有 new 建構子的情況下呼叫 Date() 函數會以字串而不是 Date 物件的形式傳回日期和時間

  • Date():以字串形式傳回日期和時間。

務必記住這兩種類型之間的區別,因為它會影響可用的操作、資訊的儲存方式以及它將為您帶來的彈性。一般來說,幾乎總是最好使用 Date 類型儲存日期資訊,然後根據需要將其格式化以供輸出。

讓我們看看這在 MongoDB shell 會話中是如何運作的。

首先,我們可以切換到新的臨時資料庫並建立三個文件,每個文件都有一個 date 欄位。我們使用不同的方法來填入每個物件的 date 欄位

use temp_db
db.dates.insertMany([
{
name: "Created with `Date()`",
date: Date(),
},
{
name: "Created with `new Date()`",
date: new Date(),
},
{
name: "Created with `ISODate()`",
date: ISODate(),
},
])
{
"acknowledged" : true,
"insertedIds" : [
ObjectId("62726af5a3dc7398b97e6e93"),
ObjectId("62726af5a3dc7398b97e6e94"),
ObjectId("62726af5a3dc7398b97e6e95")
]
}

預設情況下,這些機制中的每一個都將儲存目前的日期和時間。您可以透過新增 ISO 8601 格式的日期字串 作為引數來儲存不同的日期和時間

db.dates.insertMany([
{
name: 'Future date',
date: ISODate('2040-10-28T23:58:18Z'),
},
{
name: 'Past date',
date: new Date('1852-01-15T11:25'),
},
])

這些將在適當的日期和時間建立 Date 物件。

需要注意的一點是,上面第一個新文件中包含尾碼 Z。這表示日期和時間是以 UTC 格式提供的。在不使用 Z 的情況下指定日期將導致 MongoDB 根據目前的當地時間解釋輸入 (儘管它始終會轉換並將其儲存為 UTC 日期在內部)。

驗證日期物件的類型

接下來,我們可以顯示結果文件,看看 MongoDB 如何儲存日期資料

db.dates.find().pretty()
{
"_id" : ObjectId("62726af5a3dc7398b97e6e93"),
"name" : "Created with `Date()`",
"date" : "Wed May 04 2022 12:00:53 GMT+0000 (UTC)"
}
{
"_id" : ObjectId("62726af5a3dc7398b97e6e94"),
"name" : "Created with `new Date()`",
"date" : ISODate("2022-05-04T12:00:53.307Z")
}
{
"_id" : ObjectId("62726af5a3dc7398b97e6e95"),
"name" : "Created with `ISODate()`",
"date" : ISODate("2022-05-04T12:00:53.307Z")
}
{
"_id" : ObjectId("62728b57a3dc7398b97e6e96"),
"name" : "Future date",
"date" : ISODate("2040-10-28T23:58:18Z")
}
{
"_id" : ObjectId("62728c5ca3dc7398b97e6e97"),
"name" : "Past date",
"date" : ISODate("1852-01-15T11:25:00Z")
}

正如預期的那樣,使用 ISODate()new Date() 填入的 date 欄位包含 Date 物件 (包裝在 ISODate 輔助函數中)。相反地,由裸 Date() 函數呼叫填入的欄位會儲存為字串。

您可以透過對集合呼叫 map 函數來驗證哪些 date 欄位包含實際的 Date 物件。map 檢查每個 date 欄位,以查看它儲存的物件是否為 Date 類型的實例,並在新欄位 (名為 is_a_Date_object) 中顯示結果。此外,我們將使用 valueOf() 方法來顯示每個 date 欄位在 MongoDB 中的實際儲存方式

db.dates.find().map(function (date_doc) {
date_doc['is_a_Date_object'] = date_doc.date instanceof Date
date_doc['date_storage_value'] = date_doc.date.valueOf()
return date_doc
})
;[
{
_id: ObjectId('62726af5a3dc7398b97e6e93'),
name: 'Created with `Date()`',
date: 'Wed May 04 2022 12:00:53 GMT+0000 (UTC)',
is_a_Date_object: false,
date_storage_value: 'Wed May 04 2022 12:00:53 GMT+0000 (UTC)',
},
{
_id: ObjectId('62726af5a3dc7398b97e6e94'),
name: 'Created with `new Date()`',
date: ISODate('2022-05-04T12:00:53.307Z'),
is_a_Date_object: true,
date_storage_value: 1651665653307,
},
{
_id: ObjectId('62726af5a3dc7398b97e6e95'),
name: 'Created with `ISODate()`',
date: ISODate('2022-05-04T12:00:53.307Z'),
is_a_Date_object: true,
date_storage_value: 1651665653307,
},
{
_id: ObjectId('62728b57a3dc7398b97e6e96'),
name: 'Future date',
date: ISODate('2040-10-28T23:58:18Z'),
is_a_Date_object: true,
date_storage_value: 2235081498000,
},
{
_id: ObjectId('62728c5ca3dc7398b97e6e97'),
name: 'Past date',
date: ISODate('1852-01-15T11:25:00Z'),
is_a_Date_object: true,
date_storage_value: -3722502900000,
},
]

這證實了顯示為 ISODATE(...) 的欄位是 Date 類型的實例,而使用裸 Date() 函數建立的 date 不是。

此外,上面的輸出顯示,使用 Date 類型儲存的物件會記錄為帶符號的整數。正如預期的那樣,與 1852 年的日期關聯的日期物件是負數,因為它是從 1970 年 1 月開始倒數的。

查詢日期物件

如果您的集合中混合了像這樣的日期表示形式,您可以使用 $type 運算子查詢具有相符類型的欄位。

例如,若要查詢 dateDate 物件的所有文件,您可以輸入

db.dates
.find({
date: { $type: 'date' },
})
.pretty()
{
"_id" : ObjectId("62726af5a3dc7398b97e6e94"),
"name" : "Created with `new Date()`",
"date" : ISODate("2022-05-04T12:00:53.307Z")
}
{
"_id" : ObjectId("62726af5a3dc7398b97e6e95"),
"name" : "Created with `ISODate()`",
"date" : ISODate("2022-05-04T12:00:53.307Z")
}
{
"_id" : ObjectId("62728b57a3dc7398b97e6e96"),
"name" : "Future date",
"date" : ISODate("2040-10-28T23:58:18Z")
}
{
"_id" : ObjectId("62728c5ca3dc7398b97e6e97"),
"name" : "Past date",
"date" : ISODate("1852-01-15T11:25:00Z")
}

若要尋找 date 欄位改為儲存為字串的實例,請輸入

db.dates
.find({
date: { $type: 'string' },
})
.pretty()
{
"_id" : ObjectId("62726af5a3dc7398b97e6e93"),
"name" : "Created with `Date()`",
"date" : "Wed May 04 2022 12:00:53 GMT+0000 (UTC)"
}

Date 類型允許您執行了解時間單位之間關係的查詢。

例如,您可以像比較其他類型一樣,依序比較 Date 物件。若要檢查未來的日期,您可以輸入

db.dates
.find({
date: {
$gt: new Date(),
},
})
.pretty()
{
"_id" : ObjectId("62728b57a3dc7398b97e6e96"),
"name" : "Future date",
"date" : ISODate("2040-10-28T23:58:18Z")
}

如何使用 Date 類型方法

您可以使用各種包含的方法和運算子來操作 Date 物件。例如,您可以從日期中提取不同的日期和時間組成部分,並以許多不同的格式列印。

示範可能是展示此功能的最快方式。

首先,讓我們從具有日期物件的文件中選取日期

date_obj = db.dates.findOne({ name: 'Future date' }).date

現在,我們可以選取 date 欄位並從中提取不同的組成部分,方法是在物件上呼叫各種方法

date_obj.getUTCFullYear()
date_obj.getUTCMonth()
date_obj.getUTCDate()
date_obj.getUTCHours()
date_obj.getUTCMinutes()
date_obj.getUTCSeconds()
2040 // year
9 // month
28 // date
23 // hour
58 // minutes
18 // seconds

還有一些配套方法可用於透過提供不同的時間和日期組成部分來設定時間。例如,您可以透過呼叫 .setUTCFullYear() 方法來變更年份

date_obj.toString()
date_obj.setUTCFullYear(2028)
date_obj.toString()
date_obj.setUTCFullYear(2040)
Sun Oct 28 2040 23:58:18 GMT+0000 (UTC)
1856390298000 // integer stored for the new date value
Sat Oct 28 2028 23:58:18 GMT+0000 (UTC)
2235081498000 // integer stored for the restored date value

我們也可以將日期轉換為不同的格式以供顯示

date_obj.toDateString()
date_obj.toUTCString()
date_obj.toISOString()
date_obj.toLocaleDateString()
date_obj.toLocaleTimeString()
date_obj.toString()
date_obj.toTimeString()
Sun Oct 28 2040 // .toDateString()
Sun, 28 Oct 2040 23:58:18 GMT // .toUTCString()
2040-10-28T23:58:18.000Z // .toISOString()
10/28/2040 // .toLocaleDateString()
23:58:18 // .toLocaleTimeString()
Sun Oct 28 2040 23:58:18 GMT+0000 (UTC) // .toString()
23:58:18 GMT+0000 (UTC) // .toTimeString()

這些主要都是與 JavaScript 的 Date 類型關聯的方法。

如何使用 MongoDB Date 聚合函數

MongoDB 也提供了一些其他可以操作日期的函數。$dateToString() 聚合函數 就是一個有用的範例。您可以傳遞 Date 物件、格式字串指定符和時區指示器來呼叫 $dateToString()。MongoDB 將使用格式字串作為範本,以了解如何輸出給定的 Date 物件,並使用時區來正確偏移 UTC 的輸出。

在這裡,我們將使用任意字串格式化 dates 集合中的日期。我們也會將日期轉換為紐約時區。

首先,我們需要移除任何可能將 date 欄位儲存為字串的雜散文件

db.dates.deleteMany({ date: { $type: 'string' } })

現在我們可以執行使用 $dateToString 函數的聚合

db.dates
.aggregate([
{
$project: {
_id: 0,
date: '$date',
my_date: {
$dateToString: {
date: '$date',
format:
'Day %d of Month %m (Day %j of year %Y) at %H hours, %M minutes, and %S seconds (timezone offset: %z)',
timezone: 'America/New_York',
},
},
},
},
])
.pretty()
{
"date" : ISODate("2022-05-04T12:00:53.307Z"),
"my_date" : "Day 04 of Month 05 (Day 124 of year 2022) at 08 hours, 00 minutes, and 53 seconds (timezone offset: -0400)"
}
{
"date" : ISODate("2022-05-04T12:00:53.307Z"),
"my_date" : "Day 04 of Month 05 (Day 124 of year 2022) at 08 hours, 00 minutes, and 53 seconds (timezone offset: -0400)"
}
{
"date" : ISODate("2040-10-28T23:58:18Z"),
"my_date" : "Day 28 of Month 10 (Day 302 of year 2040) at 19 hours, 58 minutes, and 18 seconds (timezone offset: -0400)"
}
{
"date" : ISODate("1852-01-15T11:25:00Z"),
"my_date" : "Day 15 of Month 01 (Day 015 of year 1852) at 06 hours, 28 minutes, and 58 seconds (timezone offset: -0456)"
}

$dateToParts() 函數同樣有用。它可用於將 Date 欄位分解為其組成部分。

例如,我們可以輸入

db.dates.aggregate([
{
$project: {
_id: 0,
date: {
$dateToParts: { date: '$date' },
},
},
},
])
{ "date" : { "year" : 2022, "month" : 5, "day" : 4, "hour" : 12, "minute" : 0, "second" : 53, "millisecond" : 307 } }
{ "date" : { "year" : 2022, "month" : 5, "day" : 4, "hour" : 12, "minute" : 0, "second" : 53, "millisecond" : 307 } }
{ "date" : { "year" : 2040, "month" : 10, "day" : 28, "hour" : 23, "minute" : 58, "second" : 18, "millisecond" : 0 } }
{ "date" : { "year" : 1852, "month" : 1, "day" : 15, "hour" : 11, "minute" : 25, "second" : 0, "millisecond" : 0 } }

MongoDB 關於聚合函數的文件提供了有關可用於操作 Date 物件以進行顯示或比較的其他函數的資訊。

結論

在本指南中,我們介紹了一些您可以在 MongoDB 中處理日期和時間資料的不同方式。大多數時間資料可能應該儲存在 MongoDB 的 Date 資料類型中,因為這在操作資料或顯示資料時提供了很大的彈性。

熟悉日期和時間資料在內部如何儲存、如何將其強制轉換為所需的輸出格式,以及如何比較、修改和將資料分解為有用的區塊,可以幫助您解決許多不同的問題。雖然日期資訊可能很難處理,但利用可用的方法和運算子可以幫助減輕一些繁重的工作。

關於作者
Justin Ellingwood

Justin Ellingwood

Justin 自 2013 年以來一直撰寫關於資料庫、Linux、基礎架構和開發人員工具的文章。他目前與妻子和兩隻兔子住在柏林。他通常不必以第三人稱寫作,這對所有相關方來說都是一種解脫。